import * as events from 'events'
import * as stream from 'stream'
import os, { EOL } from 'os'
import child from 'child_process'

export class CommandInvoker extends events.EventEmitter {
    constructor(command: string, args?: string[], workPath?:string, options?: CommandOptions) {
        super()

        this.command = command
        this.args = args || []
        this.workPath = workPath || ''
        this.options = options || {}
    }

    private readonly command: string
    private readonly args: string[]
    private readonly workPath: string
    private readonly options: CommandOptions

    private stdoutBuffer = ''
    private stderrBuffer = ''

    private aggregateData(
        data: Buffer,
        restData: string,
        onLineData: OnLineFunc
    ): string {
        try {
            let str = data.toString().replace(/\n/g, os.EOL)
            let content = restData + str
            let eolIndex = content.indexOf(os.EOL)

            while (eolIndex > -1) {
                const line = content.substring(0, eolIndex)
                onLineData(line)

                content = content.substring(eolIndex + os.EOL.length)
                eolIndex = content.indexOf(os.EOL)
            }

            return content
        } catch (err) {
            return ''
        }
    }

    async callCommand(): Promise<number> {
        return new Promise<number>((resolve, reject) => {
            if (this.options.outStream) {
                this.options.outStream.write(this.getCommandString() + os.EOL)
            }

            const commandProcess = child.spawn(this.command, this.args, {cwd: this.workPath})
            if (commandProcess.stdout) {
                commandProcess.stdout.on('data', (data: Buffer) => {
                    if (this.options.listener && this.options.listener.stdout) {
                        this.options.listener.stdout(data)
                    }

                    if (this.options.outStream) {
                        this.options.outStream.write(data)
                    }

                    this.stdoutBuffer = this.aggregateData(
                        data,
                        this.stdoutBuffer,
                        (line: string) => {
                            if (
                                this.options.listener &&
                                this.options.listener.stdoutLine
                            ) {
                                this.options.listener.stdoutLine(line)
                            }
                        }
                    )
                })
            }

            if (commandProcess.stderr) {
                commandProcess.stderr.on('data', (data: Buffer) => {
                    if (this.options.listener && this.options.listener.stderr) {
                        this.options.listener.stderr(data)
                    }

                    if (this.options.errStream) {
                        this.options.errStream.write(data)
                    }

                    this.stderrBuffer = this.aggregateData(
                        data,
                        this.stderrBuffer,
                        (line: string) => {
                            if (
                                this.options.listener &&
                                this.options.listener.stderrLine
                            ) {
                                this.options.listener.stderrLine(line)
                            }
                        }
                    )
                })
            }

            const onErrorFunc = (err: Error): void => {
                this.flushBuffer()
                reject(err)
            }

            const onFinishFunc = (code: number): void => {
                this.flushBuffer()
                resolve(code)
            }

            commandProcess.on('error', onErrorFunc)
            commandProcess.on('exit', onFinishFunc)
            commandProcess.on('close', onFinishFunc)
        })
    }

    private flushBuffer() {
        if (
            this.stderrBuffer.length > 0 &&
            this.options.listener &&
            this.options.listener.stderrLine
        ) {
            this.options.listener.stderrLine(this.stderrBuffer)
            this.stderrBuffer = ''
        }

        if (
            this.stdoutBuffer.length > 0 &&
            this.options.listener &&
            this.options.listener.stdoutLine
        ) {
            this.options.listener.stdoutLine(this.stdoutBuffer)
            this.stdoutBuffer = ''
        }
    }

    private getCommandString(): string {
        let cmdStr = ''
        cmdStr += this.command
        for (let i = this.args.length - 1; i >= 0; i--) {
            cmdStr += ` ${this.args[i]}`
        }

        return cmdStr
    }
}

export class CommandOptions {
    outStream?: stream.Writable
    errStream?: stream.Writable

    listener?: CommandListener
}

export interface CommandResult {
    exitCode: number
    stdoutContent: string
    stderrContent: string
}

export interface CommandListener {
    stdout?: DataHandlerFunc
    stderr?: DataHandlerFunc

    stdoutLine?: LineHandlerFunc
    stderrLine?: LineHandlerFunc
}

export type DataHandlerFunc = (data: Buffer) => void
export type LineHandlerFunc = (line: string) => void
export type OnLineFunc = (line: string) => void
